feat(client)!: converge analytics.query / meta / explain and automation.trigger on unwrapResponse — one SDK calling convention (#13079) - #14526
Conversation
…rigger on unwrapResponse
The four dispatcher-served methods ended `return res.json()` and handed their
callers the `{ success, data }` envelope whole, while every other
dispatcher-served method of ObjectStackClient strips it through
`unwrapResponse`. All four now end `return this.unwrapResponse(res)` and
declare the payload: `AnalyticsResult`, `AnalyticsMetadataResponse['data']`,
`AnalyticsSqlResponse['data']`, `AutomationResult` (the value
`automation.execute` already answered for the same handler).
`analytics.queryDataset` is protected: served bare by @objectstack/rest, it
keeps `res.json()`; its body is byte-identical (only its docblock says why).
Tests: `envelope-convergence.test.ts` (new, mocked transport: the payload on
all four, the unchanged rejection path, the no-`data` pass-through, and the
protected method); `analytics-automation-json-erasure.test.ts` re-driven
against the real producers to the payload; `return-type-precision.test.ts`
pins reversed (payload compiles, `.data` is the type error); the caller
census regenerated to the post-convergence ledger with the cloud census
command kept and reworded. One breaking changeset with the migration table.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…t property
`queryDataset` is the last member of `analytics = {…}` and closes with a bare
`}`; the slicer required `},` and could not find it inside the block.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…next sibling The previous anchor (a method's own closing brace) stopped at queryDataset's parameter type literal, and a `\s+` indentation capture absorbed newlines. Comments are masked first so docblock prose cannot satisfy a code assertion. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
📓 Docs Drift CheckThis PR changes 2 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 131 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2e3b58878aea4c7c019d472ba2342f3768d3deb5 && git checkout 2e3b58878aea4c7c019d472ba2342f3768d3deb5
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2cc4610304c58c3de05a36efbb7cd1710b9d440e 5dc1af239ea89af464e9764243936b3fee585497 && git checkout -B drift-repro 2cc4610304c58c3de05a36efbb7cd1710b9d440e && git merge --no-ff 5dc1af239ea89af464e9764243936b3fee585497
node scripts/docs-audit/affected-docs.mjs --json 2cc4610304c58c3de05a36efbb7cd1710b9d440e
|
…he docblock's
`analytics/dataset/query` only ever appeared in prose; the method spells it
`getRoute('analytics')` + `${route}/dataset/query`, which is what a
comment-masked slice can see.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
… level Contract review corrections, prose only. `client.automation.trigger` is excepted from the universal compile-error claim: `r.success` and `r.error` compile before and after (both declared on `AutomationResult`), and their meaning moves from the envelope's to the run's own — a refusal the door does not classify as 400/409/422 is answered 200 through `deps.success(result)` (`respondToFlowTrigger`, classification in `flow-dispatch-status.ts`), as `execute` already does. The "not a value you will receive" sentence is scoped to the envelope level, and the `meta` sentence no longer implies the four doors ever populated it. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipza
…ledger (#13079) The changeset for this branch carried `adr-0087: not-required (runtime-interface-only …)` and the gate refused it: `runtime-interface-only` inherits the migration-prescription refusal, and this body ships a `## Migration` table. The truthful disposition is `registered`. The ledger already carries two entries of exactly this class on this package, landed four days before this branch — `client-delete-result-success` and `client-meta-reset-result-reset`, both `@objectstack/client` return-shape moves, both "same call, same wire body", both pure `unwrapResponse` passthroughs with no `packages/spec` schema movement. This one is the stronger case of the three: those corrected declarations that were UNINHABITED, so they revealed a defect rather than breaking working code, whereas this moves reads that work today. - `entries/semantic/18.client-envelope-convergence-analytics-automation.ts` — one `SemanticMigration` for major 18, the open window. surface: the resolved value of `client.analytics.query` / `.meta` / `.explain` and `client.automation.trigger`, from the dispatcher's `{ success, data }` envelope to `data`. replacement: the payload, `r.data.X` -> `r.X`, the four rows of this branch's own `## Migration` table. reason: one SDK calling convention, and the wire is byte-identical — this branch's diff touches no `packages/spec` path at all (`git diff --name-only <merge-base> HEAD -- packages/spec` is empty). `tsc` names every site on the three analytics methods (TS2339), but NOT on `automation.trigger`: `AutomationResult` itself declares `success: boolean` and `error?: string` (`packages/spec/src/contracts/automation-service.ts`, byte-identical at the merge base and here), so `r.success` / `r.error` compile on both sides while their subject moves from the envelope to the run. An untyped JS caller has no channel at all, which is why the ledger entry is the only notification that reaches it. acceptanceCriteria: no code reads `.data` off the four, and every branch on `r.success` / `r.error` off `trigger` is re-read by hand. - `migrations/registry.ts` — regenerated by `gen:migration-registry` (144 semantic, 75 retired-key, 88 retired-def); never hand-edited. - The changeset's ADR-0087 marker moves to `registered client-envelope-convergence-analytics-automation`; the rest of the file is byte-identical (one line changed). `gen:spec-changes` and `gen:upgrade-guide` were re-run and write no diff: `PROTOCOL_VERSION` is 17.0.0, so both artifacts project majors up to 17 and neither carries any step-18 entry yet — the 2026-08-29 precedent `client-meta-reset-result-reset` appears zero times in either file too. Issue anchors live in a source comment at the top of the entry rather than in its strings: the strings are projected into `spec-changes.json` and `docs/protocol-upgrade-guide.md` when 18 becomes current, and this repo's issue numbers do not resolve for the consumers who read those. Registration requested on #14996; the landing PR is #14526, which stays the `domain:cli` seat's to land. Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i Co-authored-by: Claude <noreply@anthropic.com>
One additive commit: the ADR-0087 registration this branch's changeset needs
Requested by the What the commit adds (3 files, +179 / -1):
The gate, before and after — Before, at After, at Same verdict with Reverse-verified from the committed state: removing the entry file, regenerating the registry and committing that (entry absent at HEAD, id occurrences in Also green at
Generated by Claude Code Generated by Claude Code |
… census does not count its prose (#13079) `packages/client/src/envelope-caller-census.test.ts` (§3, this branch's own ratchet) walks the whole workspace for `analytics.query|meta|explain (` and `automation.trigger (` — over comment-masked source, so a docblock is exempt but a STRING LITERAL is not — and requires every hit to be classified in its hand ledger. The entry added in 24f2507 wrote the four methods in their call-site spelling inside its `surface` / `replacement` / `acceptanceCriteria` prose, so the census counted 9 sites in the entry file and the same 9 in the generated `registry.ts`: `sdkSites.length` 46 instead of 28, 18 unclassified, `Test Core (1/6)` red on 24f2507 while the same shard was green on d543e09. Fixed inside this registration's own surface only. Every mention of the four is respelled so no `(` follows the method name, whitespace included: - surface — `client.automation.trigger (the resolved value …)` becomes an em dash and a colon, no parenthesis and no closing one - replacement — `client.analytics.query(q)` / `.meta(cube?)` / `.explain(q)` / `client.automation.trigger(name, payload)` now name their arguments in words ("called with a query", "with or without a cube name", "given a trigger name and a payload"), and the four rewrite rows keep their `r.data.X` → `r.X` spelling exactly, which the census does not match - acceptanceCriteria — the four `client.…()` forms lose their empty parens ⛔ The census test itself is untouched: it is the `domain:cli` seat's file, and the fix belongs on the side that introduced the text. Verified with the census's own matcher (its regex, over the tree's own `maskComments`): the entry file and `registry.ts` now yield 0 hits each, down from 9 and 9. `pnpm --filter @objectstack/client exec vitest run src/envelope-caller-census.test.ts` — 14 passed, including `sdkSites.length === 28`, the mechanical-enumeration-vs-hand-ledger equality and the zero-unclassified assertion. `registry.ts` regenerated by `gen:migration-registry`; no other file touched, and the entry's meaning is unchanged. Claude-Session: https://claude.ai/code/session_0174WZTU6XcFcS7g2kykC53i Co-authored-by: Claude <noreply@anthropic.com>
Patch commit
|
Fixes #13079
Executes the maintainer's ruling on the card (2026-08-31, comment
5479237368, verbatim and untranslated): 「裁决:A,cloud 未测量照裁」 — 「四方法(analytics.query/analytics.meta/analytics.explain/automation.trigger)收敛unwrapResponse,SDK 一套读法。」 Ruling items honoured: (1)analytics.queryDatasetprotected — its method body is byte-identical to base (sha12e6ec790…, 467 bytes; only its docblock says why it stays onres.json()); (2) migration note + breaking changeset; the 13 loud in-repo pins PR #13647 measured are this diff; (3) objectui's tolerant chain is objectui#7028's, time-gated behind this landing — not touched here; (4)cloudstays NOT MEASURED:CLOUD_CENSUS_COMMANDis kept in the census file, reworded to post-convergence semantics; (5) clause ② YES —needs:contract-reviewapplied at creation. Session: https://claude.ai/code/session_016yfqQh2dBgPAymYd7xipzaContract change — per method, before and after
unwrapResponsenever throws. Every non-2xx answer is thrown byObjectStackClient.fetchBEFORE either reader runs, with the ADR-0112 envelope on the error (err.code,err.httpStatus,err.message,err.details) — that was true before this PR and is true after it. A 2xx body with a booleansuccessand adatakey now resolves todata; a 2xx body with nodatakey resolves unchanged (pass-through). No dispatcher door behind these four routes sends a 2xx withoutdata(a failed run has been a thrown 400 since #9378), so at the ENVELOPE level a resolved{ success: false, error }is not a value a caller receives from them. At the PAYLOAD level one door differs:client.automation.triggercan resolve to anAutomationResultwhose ownsuccessisfalse(witherrorset) — a run thatclassifyFlowRefusal(packages/runtime/src/flow-dispatch-status.ts) does not classify as 400FLOW_FAILED/ 409FLOW_DISABLED/ 422FLOW_NO_START_NODEis answered 200 throughdeps.success(result)inrespondToFlowTrigger(packages/runtime/src/domains/automation.ts), exactly asclient.automation.executealready does for the same handler; before this PR that run reached the caller as{ success: true, data: { success: false, error } }. Consequence for a caller: ontrigger,r.successandr.errorcompile before AND after (both declared onAutomationResult,packages/spec/src/contracts/automation-service.ts), but their meaning moves from the envelope's (successalwaystrueon a resolved call,errornever set on a 2xx) to the run's own — a branch on either must be re-read by hand, the compiler will not point at it. The three analytics methods have no such overlap:.datais TS2339 and nothing else compiles on both sides.analytics.query(q)POST /analytics/query{ success: true, data: AnalyticsResult, meta? }— declaredBaseResponse & { data: AnalyticsResult }AnalyticsResult(rows,fields,sql?,totals?)VALIDATION_ERRORon a refused queryanalytics.meta(cube?)GET /analytics/metaAnalyticsMetadataResponse—{ success, data: CubeMeta[], meta? }AnalyticsMetadataResponse['data']— the bare cube listanalytics.explain(q)POST /analytics/sqlAnalyticsSqlResponse—{ success, data: { sql, params }, meta? }AnalyticsSqlResponse['data']—{ sql, params }automation.trigger(name, payload)POST /automation/trigger/:name{ success: true, data: AutomationResult, meta? }— declaredBaseResponse & { data: AutomationResult }AutomationResult— the same valueautomation.executeanswers for the same handler (its ownsuccessmay befalseon an unclassified 200 refusal)FLOW_FAILED(#9378), 409FLOW_DISABLED/ 422FLOW_NO_START_NODE(#9415), 404 unknown flow — unchangedanalytics.queryDataset(...)— PROTECTEDPOST /analytics/dataset/query(@objectstack/rest, bare)AnalyticsResultAnalyticsResult— unchanged, stillres.json()What a caller loses: the envelope's
successflag — alwaystrueon a resolved call — is no longer on the resolved value of these four; itsmetaslot is gone too, but these doors never populated it (each answersdeps.success(result)with no meta argument, and JSON drops theundefined). Neither key was ever on any other SDK method's value. What a caller gains: one reading rule;triggerandexecutestop answering two shapes for one handler. The wire is untouched: every route answers exactly the body it answered before, so raw-HTTP callers are unaffected.Diff
packages/client/src/index.ts— the four methods endreturn this.unwrapResponse(res); return declarations move to the payload types, derived from the existing envelope types where the spec transcribes them (AnalyticsMetadataResponse['data'],AnalyticsSqlResponse['data']) and to the producer contracts otherwise (AnalyticsResult,AutomationResult) — nopackages/specedit. TheBaseResponseimport goes (unused). Docblocks rewritten so nothing documents the envelope read;queryDataset's docblock says why it stays.packages/client/src/envelope-convergence.test.ts(new) — mocked transport: each of the four resolves todatawith the envelope keys absent; exactly-once strip on a payload carrying its ownsuccess; the rejection path per door (400FLOW_FAILED, 409FLOW_DISABLED, 400VALIDATION_ERROR) unchanged; the 2xx no-datapass-through pinned;queryDatasetresolves to the bare body. RED on the base in exactly its five payload cases (the five failure-path/protected cases pass there, by design).packages/client/src/return-type-precision.test.ts— the A sixth client-SDK erasure spelling, larger than the other five combined: 43 exported methodsreturn res.json()directly, whose lib.dom type isPromise< any >#12104 pins REVERSED, not deleted:expectTypeOfbinds the payload types; the four@ts-expect-errorsuppressions move from the payload read to the.dataread. RED on the base as 4×TS2344 + 4×TS2578.packages/client/src/analytics-automation-json-erasure.test.ts— the four cases re-driven against the realAnalyticsService/AutomationEngine/HttpDispatcherto the payload; the premise case now pins "dispatcher wraps once, SDK strips once" (meta()equals the dispatcher body'sdata).packages/client/src/envelope-caller-census.test.ts— the derived ledger regenerated to the post-convergence world:ENVELOPE_DEPENDENTratcheted at ZERO, newPAYLOAD_DEPENDENTverdict (18),RESULT_INSENSITIVE10,NOT_SDK1, 28 SDK sites; section 4 reads off the SDK source (comment-masked, namespace-anchored, sliced to the next sibling property) that the four end inunwrapResponseandqueryDatasetstill dialsgetRoute('analytics')+${route}/dataset/queryand readsres.json(); the split-call positive control moves from 2 to 5;CLOUD_CENSUS_COMMANDkept, reworded (a.dataread there is now a runtime break); objectui block names objectui#7028 as the tightening owner.packages/client/src/client.test.ts— one stale comment (the six RESULT_INSENSITIVE sites there are untouched)..changeset/client-envelope-convergence.md—@objectstack/client: minorunder the lockstep launch-window convention (check-changeset-no-major.mjs: the repo never writesmajor;**BREAKING**+feat(client)!:are the carriers), with the per-method migration table, the failure-path statement, and the ADR-0087 marker. Patch round after the contract review: the failure-path claims are scoped to the envelope level,trigger's ownsuccess/errorare excepted from the compile-error claim, and themetasentence no longer implies these doors populated it — prose only, no code change.Out of scope and untouched: the ~40 other
return res.json()sites inindex.ts(the better-auth-backedauth.*/organizations.*/oauth.*families, a different class: REST-served bare bodies, not dispatcher envelopes).content/docs/api/client-sdk.mdx:312–329,433anddata-api.mdx:443mention the four; none shows a.dataread on the SDK value, so no devx edit is strictly required — reported for the PM, not edited.packages/client/README.md:273–277likewise.⚠ Known red, recorded rather than worked around:
check:adr-0087-registrationThe changeset states
not-required (runtime-interface-only packages/client/src/index.ts#ObjectStackClient)and the gate refuses it (node scripts/check-adr-0087-registration.mjs --base 1dcb995f23→ exit 1):runtime-interface-onlyinherits the prescription refusal (#8299) and the body carries the## Migrationtable the ruling requires;type-surface-only(#13080) is scoped by its predicate 4 to symbols that wereanyat the merge base, and these four were concrete envelope types there;registeredwould put an SDK reader change into a ledger that servesobjectstack migrate meta. No verifiable category fits a concrete-to-concrete published runtime-interface move that ships its prescription. Filed as #14502 (spec-tooling); precedent for landing with the refusal recorded in-body:.changeset/adr0006-d2-client-environments-namespace.md. Thechangeset-checkjob (pr-automation.yml) is advisory, not one of the six required contexts.Verification
Readings are quoted at the sha they were taken; exit codes were captured before any pipe; every build/test that acquired the shared verify lock (
scripts/pm/os-verify-lock.sh) ended with VERDICTcommand-exit 0on the wrapper. The patch-round commit changes only.changeset/client-envelope-convergence.md(prose), so every reading below stands for the code at the new head.1dcb995f23(untouched tree, dependency closure freshly built — 103 packages): census + producer-backed suite →Test Files 2 passed (2) · Tests 20 passed (20). The Census the caller population of the four envelope-returning SDK methods (#13079) #13647 ledger (13 ENVELOPE_DEPENDENT / 6 / 1, 19 SDK sites) holds on this base.1dcb995f23, only the new suite and the reversed pins present,index.tsunconverted:envelope-convergence.test.ts→Tests 5 failed | 5 passed (10)— exactly the five payload cases red;tsc --noEmit -p tsconfig.test.json→ 17 errors: 4×TS2344 + 4×TS2578 inreturn-type-precision.test.ts, 9 payload reads in the new suite.897585a74b:pnpm --filter @objectstack/client buildexit 0;pnpm --filter @objectstack/client typecheckexit 0 (check:test-typecheck: OK — 0 file(s) / 0 error(s));check:exported-any-returns→no NEW exported callable of @objectstack/client resolves to any: 317 callables reached; full client suite →Tests 1 failed | 430 passed (431), the one red being the census's ownqueryDatasetsource-slicer (a test-file defect, fixed over5ce08b0f0c/8f9001c665/9e5a10ee5c;packages/client/src/index.tsis blob3575b137…in all four commits).8f9001c665(lock acquired): the four client suites →Tests 1 failed | 32 passed (33), the one red being the census assertion that matched the URL by its docblock spelling; tsc over the test program → 0 errors.9e5a10ee5cchanges only that assertion (getRoute('analytics')+${route}/dataset/query, the code spelling); its exact regexes were evaluated by node against the final head'sindex.ts(all four predicates true;unwrapResponseabsent) and the slicer's five slices are byte-exact (queryDataset= 467 bytes). The final-head vitest re-run of the census file is declared NOT RE-RUN locally — eight lock attempts over ~65 minutes never acquired (holders:issue-14336runB.shat 1000+ s,issue-13504,issue-13440); the node evaluation is the same predicate the test evaluates, andTest Core(required) runs the file on the PR.9e5a10ee5c, gate union (dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, re-derived at this sha; 36 families, identical to the lists derived at5ce08b0f0cand8f9001c665): 31 families exit 0 — includingcheck:query-options-erasure(ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new) andcheck:type-check-coverage;check:adr-0087-registrationexit 1 = the recorded refusal above (check-adr-0087-registration has no verifiable disposition for a published runtime-interface change that was concrete at base and ships a code prescription — every category refuses #13079's changeset #14502);check-test-completenessexit 3 (PREREQUISITE NOT MET — this gate grades a saved turbo run test log) NOT MEASURED;pm/check-half-statesexit 3 (PREREQUISITE NOT MET — repo-scoped reads are refused) NOT MEASURED;check:dual-build-cjs-loadsexit 3 (This is NOT a pass: nothing was measured, 30 packages unbuilt here) NOT MEASURED. Whole-repopnpm lint(eslint . --no-inline-config) exit 0 at9e5a10ee5c;pnpm check:nul-bytesexit 0. At the patch-round head both changeset gates re-run:check-changeset-no-majorgreen (This diff introduces no major bump),check-adr-0087-registrationthe same recorded refusal.check:skill-examples(needs@objectstack/client-reactbuilt — verdict linepackages/client-react/dist holds no .d.ts declarations — build first) andcheck:type-check-debt(a tsc re-measure over an unbuilt closure); and the DOWNSTREAM consumer typecheck (turbo run typecheck --filter='...@objectstack/client', prefix dots = consumers,^buildfirst). The narrowing's evidence: population = the six consumers pnpm names (cli,client-react,example-todo,example-crm,example-showcase,dogfood); a comment-masked, newline-tolerant, receiver-aware scan of their 723 source files finds 0 call-shaped occurrences of the four methods (positive control: the same scanner finds 29 inpackages/client/src), the census §3 asserts zero production sites workspace-wide at this head, and the only declarations this diff moves are the four methods' return types plus an unused type import — so no consumer tsc program's verdict can move.TypeScript Type Check(required) runs it on the PR.Ablation (reverse verification)
At
5ce08b0f0c(index.tsblob3575b137…, identical at the final head).automation.trigger's conversion reverted on disk — annotation back toPromise< { success: boolean; data: AutomationResult } >,return this.unwrapResponse< AutomationResult >(res)back toreturn res.json()— by exact-string replacement asserting one occurrence each. Mutation proven on disk: probe counts1 → 0(the trigger unwrap line) and44 → 45(return res.json();lines), blob3575b137…→add6b550…(the leg aborts if the blob had not moved). Direction predicted before running: RED, more diagnostics. Observed: convergence suite 2 failed (trigger payload; exactly-once strip), producer-backed suite 1 failed (trigger), census 1 failed (each of the four methods ends unwrapResponse) —Tests 5 failed | 26 passed (31)including the then-unfixed slicer case; tsc 8 errors: TS2344 (return-type-precision.test.ts:484), TS2578 (:503, the trigger.datasuppression now unused), 3×TS2339 in the producer-backed suite, 3×TS2339 in the convergence suite. Restore bygit checkout HEAD -- ABSOLUTE_PATHinside anEXIT INT TERMtrap, proven by blob back to3575b137…equal toHEAD:packages/client/src/index.ts,git diff HEAD --statempty,git status --porcelainempty; the post-restore re-run at the same sha: convergence + producer-backed + precision green, tsc 0 errors. No rebuild leg was needed: vitest resolves./indextosrc/index.tsandtsconfig.test.jsonreads src, so the mutation reached both suites without a build — the red itself is the evidence;dist/(built once at897585a74b) is read only bycheck:exported-any-returns, which was not part of the ablation.